/*
 * Copyright (c) 2010, Christian Lerche
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * Author: Christian Lerche <christian.lerche@uni-rostock.de>
 *
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/** \brief Macro to break cause of an error */
#define EXIT_ERROR(s...)	{fprintf(stdout, s); exit(EXIT_FAILURE);}

/**
 * \brief Structure that represents a text file
 * */
struct file_s{
	char* filename;
	char* filedata;
	int filelen;
	int optimized;
	int opt_index;
	char* opt_filename;
	int opt_offset;
};

static int convert_file(struct file_s *file, char* filename);
static int load_file(const char *filename, char **buf);
static int write_files(struct file_s *file, int filecount, char* filename_prefix, FILE* stream_h, char* inc);
static void optimize_files(struct file_s *file, int filecount);
static void backtrace_optimization(struct file_s *file, int filecount);
static int find_mem_in_mem(char* mem1, int mem1len, char* mem2, int mem2len);

/**
 * \brief Maximum Number of Filter Options
 *
 */
#define MAX_FILTER	           100
/** \brief Maximum String Length of an Include String*/
#define INCLUDE_STRING_LEN     500
/** \brief Maximum String Length of a filename*/
#define MAX_FILENAME_LEN       500

char filter[MAX_FILTER];
int filter_len = 0;
int verbose = 0;
int progmem_fix = 0;
int optimize = 0;
char includes[INCLUDE_STRING_LEN] = { '\0'};


#define BUF_LEN 1000
int usage(){
	printf("usage: [OPTIONS] files\n");
	printf("-h STRING   header output filename\n");
	printf("-c STRING   source output filename prefix\n");
	printf("-o          optimize size by searching fur substrings (optional)\n");
	printf("-s          print complete size (optional)\n");
	printf("-v          verbose mode (optional)\n");
	printf("-f CHAR     [0..n] filter char\n");
	printf("            possible filter are: TAB, CR, LF, SPACE\n");
	printf("-p          PROGMEM Fix for AVR Controller\n");
	printf("-i STRING   [0..n] Add an include directive like this: #include \"STRING\" \n");
	exit(1);
}

int add_filter(char* filter_arg){
	if (filter_len == MAX_FILTER -1){
		printf("Error: to many filter options\n");
		exit(1);
	}
	if (strcmp("TAB", filter_arg) == 0){
		filter[filter_len] = '\t';
		filter_len++;
		if (verbose){
			printf("filter TAB\n");
		}
		return 0;
	}
	if (strcmp("CR", filter_arg) == 0){
		filter[filter_len] = '\r';
		filter_len++;
		if (verbose){
			printf("filter CR\n");
		}
		return 0;
	}
	if (strcmp("LF", filter_arg) == 0){
		filter[filter_len] = '\n';
		filter_len++;
		if (verbose){
			printf("filter LF\n");
		}
		return 0;
	}
	if (strcmp("SPACE", filter_arg) == 0){
		filter[filter_len] = ' ';
		filter_len++;
		if (verbose){
			printf("filter SPACE\n");
		}
		return 0;
	}

	printf("Error: unknown filter %s\n", filter_arg);
	exit(1);
}
int main(int argc, char **argv){
	char *out_c_prefix=NULL, *out_filename_h=NULL;
	char *tmp, *inc;
	int opt_count = 1;
	int print_size = 0;
	int i;
	while (opt_count < argc){
		if(argv[opt_count][0] == '-'){
			switch(argv[opt_count][1]){
			case 'c': /* .c output filename prefix */
				opt_count++;
				out_c_prefix = argv[opt_count];
				break;
			case 'h': /* .h output filename */
				opt_count++;
				out_filename_h = argv[opt_count];
				break;
			case 'f': /* filter */
				opt_count++;
				add_filter(argv[opt_count]);
				break;
			case 's': /* print size */
				print_size = 1;
				break;
			case 'v': /* verbose */
				verbose = 1;
				break;
			case 'o': /* optimize */
				optimize = 1;
				break;

			case 'i': /* .c output filename */
				opt_count++;
				strcat(includes, "#include \"");
				strcat(includes, argv[opt_count]);
				strcat(includes, "\"\r\n");
				break;
			case 'p': /* progmem fix */
				progmem_fix = 1;
				break;

			default:
				/* wrong command line options */
				printf("Error: wrong command line options\n");
				usage();
			}
			/* next option */
			opt_count++;
		} else{
			/* no options anymore */
			break;
		}
	}

	if (!out_c_prefix){
		printf("Error: no source output filename defined!\n");
		usage();
	}
	if (!out_filename_h){
		printf("Error: no header output filename defined!\n");
		usage();
	}
	if (opt_count == argc){
		printf("Error: no input files\n");
		usage();
	}

	/* open output .h file */
	FILE* stream_out_h = fopen(out_filename_h, "w");
	if (stream_out_h == NULL){
		EXIT_ERROR("could not create output file (%s)\n", out_filename_h);
	}

	if ((argc == opt_count) && verbose){
		printf("Warning: no input files\n");
	}

	/*print includes*/
	inc = out_filename_h;
	tmp = strrchr(out_filename_h, '/');
	if(tmp){
		*tmp = '\0';
		inc = tmp+1;
	}

	fprintf(stream_out_h, "%s", includes);


	/* get every file, convert it and add it to the structure */
	int file_count = argc - opt_count;
	struct file_s *files = malloc(file_count * sizeof(struct file_s));
	for (i=opt_count; i< argc; i++){
		if (convert_file(&files[i-opt_count], argv[i])){
			EXIT_ERROR("convert file %s failed\n", argv[i]);
		}
	}

	if (optimize){
		optimize_files(files, file_count);
	}


	/*write output file*/
	if (write_files(files, argc-opt_count, out_c_prefix, stream_out_h, includes)){
			EXIT_ERROR("convert file %s failed\n", argv[i]);
	}

	int sum = 0, sum_opt = 0;
	if (print_size || verbose){
		for (i=0; i< file_count; i++){
			sum+=files[i].filelen;
			if (!files[i].optimized){
				sum_opt+=files[i].filelen;
			}
		}
		printf("Optimized size: %i bytes of %i bytes\n", sum_opt, sum);
	}

	/* close files and free buffers */
	for (i=0; i< file_count; i++){
		free(files[i].filedata);
		free(files[i].filename);
	}
	free(files);
	fclose(stream_out_h);

	exit(EXIT_SUCCESS);
}

static int convert_file(struct file_s *file, char* filename){
	char* file_buf, *tmp;
	char type_name[100];
	int len, i, j, ignore, c_i;
	if(verbose){
		printf("Convert %s\n", filename);
	}

	tmp = strrchr(filename, '/');
	if (tmp == NULL){
		printf("Error: Bad filename\n");
		exit(1);
	}
	tmp++; //copy from next char
	strcpy(type_name, tmp);
	tmp = strchr(type_name, '.');
	if (tmp){
		*tmp = '\0';
	}

	/* load file to memory */
	if ((len = load_file(filename, &file_buf)) <= 0){
		return -1;
	}

	file->filename = strdup(type_name);
	file->filedata = malloc(len+1); //+1 for '\0'
	file->filelen = len;
	file->opt_filename = NULL;
	file->opt_offset = 0;
	file->optimized = 0;


	c_i = 0; /* copy index*/
	for (i=0; i< len; i++){
		ignore = 0;
		/* filter this character?*/
		for (j = 0; j < filter_len; j++){
			if (file_buf[i] == filter[j]){
				ignore = 1;
				break;
			}
		}
		if (!ignore){
			file->filedata[c_i] = file_buf[i];
			c_i++;
//			fprintf(stream_c, "%#4x, ", file_buf[i]);
//			copy_len++;
//			if ((copy_len % 20) == 0){
//				fprintf(stream_c, "\n");
//			}
		}
	}

	file->filelen = c_i;
	file->filedata[c_i] = '\0'; //just for printouts
	/* free file in memory */
	free(file_buf);

	return 0;
}

static void optimize_files(struct file_s *file, int filecount) {
	int i, j, opt_res;
	/*we try to find file[i] in file [j]*/
	for (i = 0; i < filecount; i++) {
		for (j = 0; j < filecount; j++) {
			/* optimize not against me*/
			if(j == i){
				continue;
			}
			/* check if allready optimized*/
			if(file[j].optimized){
				continue;
			}
			opt_res = find_mem_in_mem(file[j].filedata, file[j].filelen, file[i].filedata, file[i].filelen);
			if (opt_res != -1){
				file[i].optimized = 1;
				file[i].opt_index = j;
				file[i].opt_offset = opt_res;
				file[i].opt_filename = file[j].filename;
				/* we are finished */
				break;
			}

		}
	}
	backtrace_optimization(file, filecount);
}

static void backtrace_optimization(struct file_s *file, int filecount){
	int i, opt_index;
	for (i = 0; i < filecount; i++) {
		opt_index = file[i].opt_index;
		if (file[opt_index].optimized){
			/* the file we refer is already optimized! */
			file[i].opt_index = file[opt_index].opt_index;
			/* add the offset*/
			file[i].opt_offset += file[opt_index].opt_offset;
			/* new filename */
			file[i].opt_filename = file[opt_index].opt_filename;
			/*NOT Optimal but easy: start from the beginning (until we found no "double" optimization)*/
			i = 0;
		}
	}
}

static int find_mem_in_mem(char* mem1, int mem1len, char* mem2, int mem2len){
	int i, end;
	if (mem1len < mem2len){
		/* mem1 is to small, mem2 can't fit*/
		return -1;
	}
	end = mem1len - mem2len;
	for (i = 0; i <= end; i++){
		if (memcmp(&mem1[i], mem2, mem2len) == 0){
			if (verbose){
				printf("found: \"%s\" in \"%s\"\n", mem2, mem1);
			}
			return i;
		}
	}


	return -1;
}

static int write_files(struct file_s *file, int filecount, char* filename_prefix, FILE* stream_h, char* inc){
	int i,j, copy_len = 0;
	char out_filename_c[MAX_FILENAME_LEN];
	FILE* stream_c;

	for (i = 0; i < filecount; i++) {
		/* open output .c file */
		sprintf(out_filename_c, "%s%s.c", filename_prefix, file[i].filename);
		stream_c = fopen(out_filename_c, "w");
		if (stream_c == NULL){
			EXIT_ERROR("could not create output file (%s)\n", out_filename_c);
		}

		fputs(inc, stream_c);

		/* print c file*/
		if (file[i].optimized) {
//			fprintf(stream_c, "const char *%s = &%s[%i];\n", file[i].filename,
//					file[i].opt_filename, file[i].opt_offset);
			//		if (progmem_fix){
			//			fprintf(stream_c, "PROGMEM ");
			//		}
		} else {
			fprintf(stream_c, "const char %s[%d] ", file[i].filename,
					file[i].filelen);
			if (progmem_fix) {
				fprintf(stream_c, "PROGMEM ");
			}
			fprintf(stream_c, "= { \n");
			for (j = 0; j < file[i].filelen; j++) {
				fprintf(stream_c, "%#4x, ", file[i].filedata[j]);
				copy_len++;
				if ((copy_len % 20) == 0) {
					fprintf(stream_c, "\n");
				}
			}
			fprintf(stream_c, "};\n");
		}

		/*print header*/
		if (file[i].optimized) {
//			fprintf(stream_h, "extern const char *%s;\n", file[i].filename);
			fprintf(stream_h, "#define %s (&%s[%i])\n", file[i].filename, file[i].opt_filename, file[i].opt_offset);
		} else {
			fprintf(stream_h, "extern const char %s[%d];\n", file[i].filename,
					file[i].filelen);
		}
		/*print length*/
		fprintf(stream_h, "#define %s_len  %u\n\n", file[i].filename,
				file[i].filelen);
//		fprintf(stream_h, "extern const int %s_len;\n\n", file[i].filename);
		fclose(stream_c);
	}
	return 0;
}


static int load_file(const char *filename, char **buf){

    int total_bytes = 0;
    int bytes_read = 0;
    int filesize;
    FILE *stream = fopen(filename, "rb");

    if (stream == NULL)
    {
    	printf("Error: open file (%s) failed\n", filename);
        return -1;
    }

    fseek(stream, 0, SEEK_END);
    filesize = ftell(stream);
    *buf = malloc(filesize);
    fseek(stream, 0, SEEK_SET);

    do
    {
        bytes_read = fread(*buf+total_bytes, 1, filesize-total_bytes, stream);
        total_bytes += bytes_read;
    } while (total_bytes < filesize && bytes_read > 0);

    fclose(stream);
    return filesize;
}
//
//static void chomp(char *str) {
//   size_t p=strlen(str);
//   /* replace '\n' with '\0' */
//   str[p-1]='\0';
//}

